<!doctype html>
<html lang="en">
	<head>
		<title>meshoptimizer - demo</title>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
		<style>
			body {
				font-family: Monospace;
				background-color: #000;
				color: #fff;
				margin: 0px;
				overflow: hidden;
			}
			#info {
				color: #fff;
				position: absolute;
				top: 10px;
				width: 100%;
				text-align: center;
				z-index: 100;
				display: block;
			}
			#info a,
			.button {
				color: #f00;
				font-weight: bold;
				text-decoration: underline;
				cursor: pointer;
			}
		</style>

		<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
		<script type="importmap">
			{
				"imports": {
					"three": "https://unpkg.com/three@0.154.0/build/three.module.js",
					"three-examples/": "https://unpkg.com/three@0.154.0/examples/jsm/"
				}
			}
		</script>
	</head>

	<body>
		<div id="info">
			<a href="https://github.com/zeux/meshoptimizer" target="_blank" rel="noopener">meshoptimizer</a>
			<div id="ratio"></div>
		</div>

		<script type="module">
			import * as THREE from 'three';
			import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js';
			import { OBJLoader } from 'three-examples/loaders/OBJLoader.js';
			import { OrbitControls } from 'three-examples/controls/OrbitControls.js';
			import { mergeVertices } from 'three-examples/utils/BufferGeometryUtils.js';
			import { MeshoptDecoder } from '../js/meshopt_decoder.module.js';
			import { MeshoptSimplifier } from '../js/meshopt_simplifier.module.js';
			import { GUI } from 'https://unpkg.com/lil-gui@0.17.0/dist/lil-gui.esm.js';

			var container;

			var camera, scene, renderer, mixer, clock, controls, model;

			var settings = {
				wireframe: false,
				animate: false,
				pointSize: 1.0,
				ratio: 1.0,
				debugOverlay: false,
				lockBorder: false,
				prune: false,
				weldVertices: false,
				useAttributes: false,
				errorThresholdLog10: 1,
				normalWeight: 0.5,
				colorWeight: 1.0,
				textureWeight: 0.0,
				autoLod: false,
				autoLodFactor: 1.0,

				stats: {
					triangles: 0,
					vertices: 0,
					error: '0',
				},

				loadFile: function () {
					var input = document.createElement('input');
					input.type = 'file';
					input.onchange = function () {
						var file = input.files[0];
						var uri = URL.createObjectURL(file);
						var ext = file.name.split('.').pop().toLowerCase();

						load(uri, ext);
						controls.reset();
					};
					input.click();
				},

				updateModule: function () {
					reload();
					simplify();
				},
				autoUpdate: false,
				autoUpdateStatus: '',
			};

			var gui = new GUI({ width: 300 });
			var guiDisplay = gui.addFolder('Display');
			guiDisplay.add(settings, 'wireframe').onChange(update);
			guiDisplay.add(settings, 'animate').onChange(update);
			guiDisplay.add(settings, 'pointSize', 1, 16).onChange(update);
			guiDisplay.add(settings, 'debugOverlay').onChange(simplify); // requires debug data rebuild

			var guiSimplify = gui.addFolder('Simplify');
			guiSimplify.add(settings, 'ratio', 0, 1, 0.01).onChange(simplify);
			guiSimplify.add(settings, 'lockBorder').onChange(simplify);
			guiSimplify.add(settings, 'prune').onChange(simplify);
			guiSimplify.add(settings, 'weldVertices').onChange(simplify);
			guiSimplify.add(settings, 'errorThresholdLog10', 0, 3, 0.1).onChange(simplify);
			guiSimplify.add(settings, 'useAttributes').onChange(simplify).onFinishChange(updateSettings);

			var guiAttributes = [];
			guiAttributes.push(guiSimplify.add(settings, 'normalWeight', 0, 2, 0.01).onChange(simplify));
			guiAttributes.push(guiSimplify.add(settings, 'colorWeight', 0, 2, 0.01).onChange(simplify));
			guiAttributes.push(guiSimplify.add(settings, 'textureWeight', 0, 100, 0.1).onChange(simplify));

			var guiLod = gui.addFolder('LOD');
			guiLod.add(settings, 'autoLod').onChange(simplify);
			guiLod.add(settings, 'autoLodFactor', 0, 10, 0.01).onChange(simplify);

			var guiLoad = gui.addFolder('Load');
			guiLoad.add(settings, 'loadFile');
			guiLoad.add(settings, 'updateModule');
			guiLoad.add(settings, 'autoUpdate').onChange(autoReload);
			guiLoad.add(settings, 'autoUpdateStatus').listen();

			var guiStats = gui.addFolder('Stats');
			guiStats.add(settings.stats, 'triangles').listen();
			guiStats.add(settings.stats, 'vertices').listen();
			guiStats.add(settings.stats, 'error').listen();

			updateSettings();
			init();
			load('pirate.glb', 'glb');
			animate();

			function updateSettings() {
				for (var i = 0; i < guiAttributes.length; ++i) {
					guiAttributes[i].enable(settings.useAttributes);
				}
			}

			function simplifyMesh(geo, threshold) {
				if (settings.weldVertices) {
					// pre-welding is important for some meshes that have very close normals in adjacent face corners
					// for now we use the welder from three.js; this is not as performant as it could be
					geo = mergeVertices(geo, 1e-2);
				}

				var attributes = 8; // 3 color, 3 normal, 2 uv

				var positions = new Float32Array(geo.attributes.position.array);
				var indices = new Uint32Array(geo.index.array); // needed for _InternalDebug to work
				var target = settings.autoLod ? 0 : Math.floor((indices.length * settings.ratio) / 3) * 3;

				if (settings.useAttributes) {
					var attrib = new Float32Array(geo.attributes.position.count * attributes);

					for (var i = 0, e = geo.attributes.position.count; i < e; ++i) {
						if (geo.attributes.normal) {
							attrib[i * attributes + 0] = geo.attributes.normal.getX(i);
							attrib[i * attributes + 1] = geo.attributes.normal.getY(i);
							attrib[i * attributes + 2] = geo.attributes.normal.getZ(i);
						}

						if (geo.attributes.color) {
							attrib[i * attributes + 3] = geo.attributes.color.getX(i);
							attrib[i * attributes + 4] = geo.attributes.color.getY(i);
							attrib[i * attributes + 5] = geo.attributes.color.getZ(i);
						}

						if (geo.attributes.uv) {
							attrib[i * attributes + 6] = geo.attributes.uv.getX(i);
							attrib[i * attributes + 7] = geo.attributes.uv.getY(i);
						}
					}

					var attrib_weights = [
						settings.normalWeight,
						settings.normalWeight,
						settings.normalWeight,
						settings.colorWeight,
						settings.colorWeight,
						settings.colorWeight,
						settings.textureWeight,
						settings.textureWeight,
					];
				}

				var flags = [];
				if (settings.lockBorder) {
					flags.push('LockBorder');
				}
				if (settings.prune) {
					flags.push('Prune');
				}

				var stride = geo.attributes.position instanceof THREE.InterleavedBufferAttribute ? geo.attributes.position.data.stride : 3;

				console.time('simplify');

				function run() {
					var S = MeshoptSimplifier; // to avoid line breaks below...
					return settings.useAttributes
						? S.simplifyWithAttributes(indices, positions, stride, attrib, attributes, attrib_weights, null, target, threshold, flags)
						: MeshoptSimplifier.simplify(indices, positions, stride, target, threshold, flags);
				}

				var res = run();

				if (settings.debugOverlay) {
					flags.push('_InternalDebug');
					var dres = run();
				}

				console.timeEnd('simplify');

				var rgeo = geo.clone();

				var dind = res[0];

				if (settings.debugOverlay) {
					// we need extra indices for debug overlay
					dind = Array.from(res[0]);

					for (var kind = 1; kind <= 4; ++kind) {
						var offset = dind.length;

						for (var i = 0; i < dres[0].length; i += 3) {
							var mask = (1 << 28) - 1;

							for (var e = 0; e < 3; ++e) {
								var a = dres[0][i + e],
									b = dres[0][i + ((e + 1) % 3)];

								if (a >> 31 != 0 && kind != 4 && (((a >> 28) & 7) == kind || ((b >> 28) & 7) == kind)) {
									// loop of current kind (non-locked to allow one of the vertices to be locked)
									dind.push(a & mask);
									dind.push(a & mask);
									dind.push(b & mask);
								} else if (kind == 4 && ((a >> 28) & 7) == kind && ((b >> 28) & 7) == kind) {
									// locked edge (may not be marked as a loop)
									dind.push(a & mask);
									dind.push(a & mask);
									dind.push(b & mask);
								}
							}
						}

						if (offset != dind.length) {
							rgeo.addGroup(offset, dind.length - offset, kind);
							offset = dind.length;
						}
					}
				}

				rgeo.index.array = new Uint32Array(dind);
				rgeo.index.count = dind.length;
				rgeo.index.needsUpdate = true;

				rgeo.addGroup(0, res[0].length, 0);

				return [rgeo, res[0].length / 3, res[1]];
			}

			function simplifyPoints(geo) {
				var positions = new Float32Array(geo.attributes.position.array);
				var colors = undefined;

				var target = Math.floor(geo.attributes.position.count * settings.ratio);

				if (settings.useAttributes && geo.attributes.color) {
					colors = new Float32Array(geo.attributes.color.array);
				}

				var pos_stride = geo.attributes.position instanceof THREE.InterleavedBufferAttribute ? geo.attributes.position.data.stride : 3;
				var col_stride =
					geo.attributes.color instanceof THREE.InterleavedBufferAttribute
						? geo.attributes.color.data.stride
						: geo.attributes.color.itemSize;

				// note: we are converting source color array without normalization; if the color data is quantized, we need to divide by 255 to normalize it
				var colorScale = geo.attributes.color && geo.attributes.color.normalized ? 255 : 1;

				console.time('simplify');

				var res = MeshoptSimplifier.simplifyPoints(positions, pos_stride, target, colors, col_stride, settings.colorWeight / colorScale);

				console.timeEnd('simplify');

				console.log('simplified to', res.length);

				geo.index = new THREE.BufferAttribute(res, 1);
			}

			function simplify() {
				MeshoptSimplifier.ready.then(function () {
					MeshoptSimplifier.useExperimentalFeatures = true;

					var rnum = 0;
					var rden = 0;

					var triangles = 0;
					var vertices = 0;
					var error = 0;

					var threshold = Math.pow(10, -settings.errorThresholdLog10);

					if (settings.autoLod) {
						// compute distance to model sphere
						// ideally this should actually use the real radius and center :) but we rescale/center the model when loading
						var center = new THREE.Vector3();
						var radius = 1;
						var distance = Math.max(camera.position.distanceTo(center) - radius, 0);
						var loderrortarget = 1e-3 * (2 * Math.tan(camera.fov * 0.5 * (Math.PI / 180))); // ~1 pixel at 1k x 1k

						// note: we are currently cutting corners wrt handling the actual mesh scale
						// since we rescale the entire scene to fit a unit box, we can directly feed the threshold
						// this works correctly if there's just one mesh or scales match; ideally we tweak this to compute
						// threshold per mesh based on relative scale between mesh and scene.
						threshold = distance * loderrortarget * settings.autoLodFactor;
					}

					scene.traverse(function (object) {
						if (object.isMesh) {
							if (!object.original) {
								object.original = object.geometry.clone();

								// use small depth offset to avoid overlay z-fighting with the original mesh
								// has to be done on the main material as overlays use lines that don't support depth offset
								object.material.polygonOffset = true;
								object.material.polygonOffsetFactor = 0.5;
								object.material.polygonOffsetUnits = 16;

								object.material = [
									object.material,
									new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true }), // border
									new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true }), // seam
									new THREE.MeshBasicMaterial({ color: 0xff00ff, wireframe: true }), // complex
									new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }), // locked
								];
							}

							var [geo, tri, err] = simplifyMesh(object.original, threshold);

							object.geometry = geo;

							error = Math.max(error, err); // note: we are ignoring the possibility of different mesh scales atm
							triangles += tri;
							vertices += object.geometry.attributes.position.count;
							rnum += tri;
							rden += object.original.index.count / 3;
						}
						if (object.isPoints) {
							simplifyPoints(object.geometry);

							vertices += object.geometry.index.count;
							rnum += object.geometry.index.count;
							rden += object.geometry.attributes.position.count;
						}
					});

					settings.stats.triangles = triangles;
					settings.stats.vertices = vertices;
					settings.stats.error = error.toExponential(3);

					document.getElementById('ratio').innerText = ((rnum / rden) * 100).toFixed(1) + '%';
				});
			}

			function reload() {
				var simp = import('/js/meshopt_simplifier.module.js?x=' + Date.now());
				MeshoptSimplifier.ready = simp.then(function (s) {
					return s.MeshoptSimplifier.ready.then(function () {
						for (var prop in s.MeshoptSimplifier) {
							MeshoptSimplifier[prop] = s.MeshoptSimplifier[prop];
						}
					});
				});
			}

			var moduleLastModified = 0;

			function autoReload() {
				if (!settings.autoUpdate) return;

				fetch('/js/meshopt_simplifier.module.js?x=' + Date.now(), { method: 'HEAD' })
					.then(function (r) {
						var last = r.headers.get('Last-Modified');
						if (last != moduleLastModified) {
							moduleLastModified = last;
							reload();
							simplify();
						}

						settings.autoUpdateStatus = new Date(last).toLocaleTimeString();
						setTimeout(autoReload, 1000);
					})
					.catch(function (e) {
						settings.autoUpdateStatus = 'error';
						setTimeout(autoReload, 5000);
					});
			}

			function update() {
				scene.traverse(function (child) {
					if (child.isMesh) {
						if (Array.isArray(child.material)) {
							child.material[0].wireframe = settings.wireframe;
						} else {
							child.material.wireframe = settings.wireframe;
						}
					}
					if (child.isPoints) {
						child.material.size = settings.pointSize;
					}
				});
			}

			function load(path, ext) {
				if (model) {
					scene.remove(model);
					model = undefined;
				}

				var onProgress = function (xhr) {};
				var onError = function (e) {
					console.log(e);
				};

				function center(model) {
					var bbox = new THREE.Box3().setFromObject(model);
					var scale = 2 / Math.max(bbox.max.x - bbox.min.x, bbox.max.y - bbox.min.y, bbox.max.z - bbox.min.z);
					var offset = new THREE.Vector3().addVectors(bbox.max, bbox.min).multiplyScalar(scale / 2);

					model.scale.set(scale, scale, scale);
					model.position.set(-offset.x, -offset.y, -offset.z);
				}

				if (ext == 'gltf' || ext == 'glb') {
					var loader = new GLTFLoader();
					loader.setMeshoptDecoder(MeshoptDecoder);
					loader.load(
						path,
						function (gltf) {
							model = gltf.scene;
							center(model);
							scene.add(model);

							mixer = new THREE.AnimationMixer(model);

							if (gltf.animations.length) {
								mixer.clipAction(gltf.animations[gltf.animations.length - 1]).play();
							}
						},
						onProgress,
						onError
					);
				} else if (ext == 'obj') {
					var loader = new OBJLoader();
					loader.load(
						path,
						function (obj) {
							obj.traverse(function (node) {
								if (node.isMesh) {
									// obj loader does not index the geometry, so we need to do it ourselves
									node.geometry = mergeVertices(node.geometry, 0.0);

									// we use groups and multiple materials for debug visualization, so merge all of the source ones for now
									if (Array.isArray(node.material)) {
										node.material = node.material[0];
										node.geometry.clearGroups();
									}
								}
							});

							model = obj;
							center(model);
							scene.add(model);
						},
						onProgress,
						onError
					);
				} else {
					console.error('Unsupported file format');
				}
			}

			function init() {
				container = document.createElement('div');
				document.body.appendChild(container);

				renderer = new THREE.WebGLRenderer();
				renderer.setPixelRatio(window.devicePixelRatio);
				renderer.setSize(window.innerWidth, window.innerHeight);
				container.appendChild(renderer.domElement);

				window.addEventListener('resize', onWindowResize, false);

				camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
				camera.position.y = 1.0;
				camera.position.z = 3.0;

				controls = new OrbitControls(camera, renderer.domElement);

				controls.addEventListener('change', function () {
					if (settings.autoLod) {
						simplify();
					}
				});

				scene = new THREE.Scene();
				scene.background = new THREE.Color(0x300a24);

				var ambientLight = new THREE.AmbientLight(0xcccccc, 0.3);
				scene.add(ambientLight);

				var pointLight = new THREE.PointLight(0xffffff, 0.8);
				pointLight.position.set(3, 3, 0);
				camera.add(pointLight);
				scene.add(camera);

				clock = new THREE.Clock();
			}

			function onWindowResize() {
				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize(window.innerWidth, window.innerHeight);
			}

			function animate() {
				if (mixer && settings.animate) {
					mixer.update(clock.getDelta());
				}

				requestAnimationFrame(animate);
				controls.update();
				renderer.render(scene, camera);
			}
		</script>
	</body>
</html>
